#define _XOPEN_SOURCE
#define _DEFAULT_SOURCE
#define TB_IMPL
#define TB_LIB_OPTS

#include "tbox.h"
#include "termbox2.h"
#include <stddef.h>
#include <string.h>

#define tbox_malloc malloc
#define tbox_free   free

enum directions {
    LL = 0,
    TT,
    RR,
    BB,
    INNER,
};

enum conner_directions {
    LT = 0,
    RT,
    RB,
    LB,
};

typedef struct node {
    struct node *prev, *next;
    tbox_widget_t *wi;
} node_t;

struct tbox_global {
    int status; // TODO: status

    int loglevel;
    FILE *logfp;

    tbox_widget_t *focus;

    node_t mouse_list;
    node_t widget_list;
    node_t layout_list;
};

static struct tbox_global gtbox;

static void log_init(struct tbox_conf *conf) {
    gtbox.logfp = NULL;

    if (conf && conf->logfile) {
        gtbox.loglevel = conf->loglevel;
        gtbox.logfp = fopen(conf->logfile, "w+");
        if (gtbox.logfp == NULL) {
            // print error
            return;
        }

        if (setvbuf(gtbox.logfp, NULL, _IONBF, 0) != 0) {
            return;
        }
    }
}

static void log_deinit(void) {
    if (gtbox.logfp) {
        fclose(gtbox.logfp);
        gtbox.logfp = NULL;
    }
}

void tbox_log(int level, const char *fmt, ...) {
    va_list vl;
    if (gtbox.logfp && level >= gtbox.loglevel) {
        va_start(vl, fmt);
        vfprintf(gtbox.logfp, fmt, vl);
        va_end(vl);
    }
}

// left-top, right-top, left-bottom, right-bottom
uint32_t conner[4] = {0x256D, 0x256E, 0x2570, 0x256F};

// top, left, right, bottom
uint32_t border[4] = {0x2500, 0x2502, 0x2502, 0x2500};

/* node list defines */
static inline void node_init_head(node_t *node) {
    node->prev = node;
    node->next = node;
}

static inline void _node_insert(node_t *node, node_t *new_node) {
    new_node->prev = node->prev;
    new_node->next = node;
    new_node->prev->next = new_node;
    new_node->next->prev = new_node;
}

static inline void node_append(node_t *head, node_t *new_node) {
    _node_insert(head, new_node);
}

#define node_for_each(pos, head) for (pos = (head)->next; pos != (head); pos = pos->next)

/* Get how many lines in string, `str` cant't be NULL */
static inline int strline(const char *str) {
    int line = 1;
    while (*str++ == '\n')
        line++;
    return line;
}

static int unilen(const char *str) {
    int len = 0;

    while (*str) {
        uint32_t uni;
        str += tb_utf8_char_to_unicode(&uni, str);
        len += tb_wcwidth((wchar_t)uni);
    }
    return len;
}

static int unirow(const char *utf8, int width) {
    int len = 0;
    int row = 1;

    while (*utf8) {
        uint32_t uni;
        utf8 += tb_utf8_char_to_unicode(&uni, utf8);
        len += tb_wcwidth((wchar_t)uni);
        if (*utf8 == '\n' || len >= width) {
            len = 0;
            row++;
        }
    }
    return row;
}

static int draw_cell(int x, int y, uint32_t unicode, uint32_t fg, uint32_t bg) {
    return tb_set_cell(x, y, unicode, fg, bg);
}

void tbox_print(const char *str, int x, int y, uint32_t fg, uint32_t bg) {
    while (*str) {
        uint32_t uni;
        str += tb_utf8_char_to_unicode(&uni, str);
        draw_cell(x, y, uni, fg, bg);
        x++;
    }
}

char *tbox_nprint(char *str, int x, int y, int n, uint32_t fg, uint32_t bg) {
    int len = 0;
    while (*str) {
        uint32_t uni;
        str += tb_utf8_char_to_unicode(&uni, str);
        len += tb_wcwidth((wchar_t)uni);
        draw_cell(x, y, uni, fg, bg);
        if (len >= n) return str;
        x++;
    }
    return str;
}

char *tbox_print_area(char *str, int x, int y, int w, int h, uint32_t fg, uint32_t bg,
    uint8_t align_h) {
    return NULL;
}

static int draw_text(tbox_widget_t *wi) {
    int x0, y0, x1, y1;

    x0 = wi->x + wi->padding[LL];
    y0 = wi->y + wi->padding[TT];
    x1 = wi->x + wi->w - wi->padding[RR];
    y1 = wi->y + wi->h - wi->padding[BB];

    if (wi->show_border) x0 += 1, y0 += 1, x1 -= 1, y1 -= 1;

    char *text = wi->_data.text;

    int row = unirow(text, x1 - x0);
    uint8_t hori = (uint8_t)(wi->align >> 4);

    // Left-Top
    if (wi->align == (TBOX_ALIGN_FORWARD << 0x4 | TBOX_ALIGN_FORWARD)) {
        if (!wi->text_wrap) {
            tbox_nprint(text, x0, y0, x1 - x0, wi->fg, wi->bg);
        } else {
            for (int yi = y0; yi < y1; yi++)
                text = tbox_nprint(text, x0, yi, x1 - x0, wi->fg, wi->bg);
        }
        return 0;
    }

    // Center
    if (wi->align == (TBOX_ALIGN_MIDDLE << 0x4 | TBOX_ALIGN_MIDDLE)) {
        if (!wi->text_wrap) {
            int len = unilen(text);
            if (len < x1 - x0) {
                x0 += (x1 - x0 - len) / 2;
            }
            tbox_nprint(text, x0, y0, x1 - x0, wi->fg, wi->bg);
        } else {
            // TODO:
        }
    }

    return 0;
}

static int clean_widget(tbox_widget_t *wi) {
    if (!wi->visible) return 0;
    uint32_t fg = wi->fg;
    uint32_t bg = wi->bg;

    int x0 = wi->x;
    int y0 = wi->y;
    int x1 = wi->x + wi->w - 1;
    int y1 = wi->y + wi->h - 1;

    for (int y = y0; y < y1; y++)
        for (int x = x0; x < x1; x++)
            draw_cell(x, y, ' ', fg, bg);

    return 0;
}

static int draw_widget(tbox_widget_t *wi) {
    if (!wi->visible) return 0;

    uint32_t fg = wi->fg;
    uint32_t bg = wi->bg;

    int x0 = wi->x;
    int y0 = wi->y;
    int x1 = wi->x + wi->w - 1;
    int y1 = wi->y + wi->h - 1;

    if (wi->show_border) {
        /* draw border conners */
        draw_cell(x0, y0, conner[0], fg, bg);
        draw_cell(x1, y0, conner[1], fg, bg);
        draw_cell(x0, y1, conner[2], fg, bg);
        draw_cell(x1, y1, conner[3], fg, bg);

        /* draw border lines: top, left, right, bottom */
        for (int xi = x0 + 1; xi < x1; xi++)
            draw_cell(xi, y0, border[0], fg, bg);

        for (int yi = y0 + 1; yi < y1; yi++)
            draw_cell(x0, yi, border[1], fg, bg);

        for (int yi = y0 + 1; yi < y1; yi++)
            draw_cell(x1, yi, border[2], fg, bg);

        for (int xi = x0 + 1; xi < x1; xi++)
            draw_cell(xi, y1, border[3], fg, bg);

        // x0 += 1, y0 += 1;
    }
#if 0
    else {
        x1 += 1, y1 += 1;
    }

    for (int y = y0; y < y1; y++)
        for (int x = x0; x < x1; x++)
            draw_cell(x, y, ' ', fg, bg);
#endif
    if (wi->_data.text) draw_text(wi);

    return 0;
}

void layout_next_widget(tbox_widget_t *wi) {
    if (wi->layout[RR]) {
        wi->layout[RR]->x = wi->x + wi->w + wi->margin[RR] + wi->layout[RR]->margin[LL];
        wi->layout[RR]->y = wi->y;
    }

    if (wi->layout[LL]) {
        wi->layout[LL]->x = wi->x - wi->margin[LL] - wi->layout[LL]->margin[RR] - wi->layout[LL]->w;
        wi->layout[LL]->y = wi->y;
    }

    if (wi->layout[TT]) {
        wi->layout[TT]->x = wi->x;
        wi->layout[TT]->y = wi->y - wi->h - wi->margin[TT] - wi->layout[TT]->margin[BB];
    }

    if (wi->layout[BB]) {
        wi->layout[BB]->x = wi->x;
        wi->layout[BB]->y = wi->y + wi->h + wi->margin[BB] + wi->layout[BB]->margin[TT];
    }
}

static void draw_each_widget(tbox_widget_t *wi) {
    if (wi == NULL) return;

    draw_widget(wi);

    layout_next_widget(wi);

    draw_each_widget(wi->layout[RR]);
    draw_each_widget(wi->layout[LL]);
    draw_each_widget(wi->layout[TT]);
    draw_each_widget(wi->layout[BB]);
    draw_each_widget(wi->layout[INNER]);
}

static void draw() {
    tbox_clear();

    node_t *root;
    node_for_each(root, &gtbox.layout_list) {
        draw_each_widget(root->wi);
    }

    tbox_flush();
}

static int set_text(tbox_widget_t *wi, char *text) {
    if (text == NULL) return -1;

    int bufsize = strlen(text) + 1;

    // alloc text buffer
    if (bufsize >= wi->_data.text_bufsize) {
        if (wi->_data.text != NULL) {
            tbox_free(wi->_data.text);
            wi->_data.text = NULL;
            wi->_data.text_bufsize = 0;
        }

        char *buf = (char *)tbox_malloc(bufsize);
        if (buf == NULL) {
            // TODO: handle error
            return -1;
        }

        // memset(buf, 0, bufsize);
        wi->_data.text = buf;
        wi->_data.text_bufsize = bufsize;
    }

    strcpy(wi->_data.text, text);
    return 0;
}

static tbox_widget_t *create_widget(int w, int h) {
    node_t *node = (node_t *)tbox_malloc(sizeof(node_t));
    if (node == NULL) return NULL;

    tbox_widget_t *wi = (tbox_widget_t *)tbox_malloc(sizeof(tbox_widget_t));
    if (wi == NULL) {
        tbox_free(node);
        node = NULL;
        return NULL;
    }

    memset(node, 0, sizeof(node_t));
    memset(wi, 0, sizeof(tbox_widget_t));

    node->wi = wi;
    node_append(&gtbox.widget_list, node);

    wi->set_text = set_text;
    wi->visible = true;
    wi->w = w;
    wi->h = h;

    return wi;
}

tbox_widget_t *tbox_button(int w, int h, char *text) {
    tbox_widget_t *button = create_widget(w, h);

    button->show_border = true;
    button->align = TBOX_ALIGN_CENTER;

    if (text != NULL) set_text(button, text);

    return button;
}

// TODO: rename tbox_layout_add_root
void tbox_layout_add(tbox_widget_t *wi) {
    node_t *node = (node_t *)tbox_malloc(sizeof(node_t));
    if (node == NULL) return;

    memset(node, 0, sizeof(node_t));

    node->wi = wi;
    node_append(&gtbox.layout_list, node);
}

inline bool tbox_focus(tbox_widget_t *wi, bool enable) {
    if (wi != NULL && wi->focusable && wi->visible) {
        if (enable) {
            gtbox.focus = wi;
        }
    }
    return false;
}

// TODO: implement find function
inline tbox_widget_t *tbox_focus_next(tbox_widget_t *wi, uint8_t directions) {
    (void)wi;
    (void)directions;
    return NULL;
}

static inline bool mouse_hit_widget(tbox_widget_t *wi, int32_t x, int32_t y) {
    if (x >= wi->x && x < wi->x + wi->w && y >= wi->y && y < wi->y + wi->h) {
        return true;
    }
    return false;
}

static void mouse_callback(tbox_widget_t *wi, tbox_event_t *ev) {
    wi->on_mouse(wi, ev);
    draw_each_widget(wi);
    tbox_flush();
}

static void handle_mouse(uint16_t key, int32_t x, int32_t y) {
    static tbox_widget_t *last_wi = NULL;
    static tbox_event_t ev = {
        .x = -1,
        .y = -1,
        .key = TBOX_KEY_MOUSE_RELEASE,
    };

    node_t *node;
    tbox_widget_t *wi;

    // check unknown mouse key
    if (key > TBOX_KEY_MOUSE_LEFT || key < TBOX_KEY_MOUSE_WHEEL_DOWN) {
        return;
    }

    /* For author's own mouse on Windows-Terminal, long press got two same key press events
     * during this period.
     * It can be used to make LONG-PRESS-EVENT, but, maybe NOT general on other environment.
     * So this feature not implemented.
     */
    bool new_pos = (x == ev.x && y == ev.y) ? false : true;
    bool new_key = (key == ev.key && key > TBOX_KEY_MOUSE_WHEEL_UP) ? false : true;

    ev.key = key;
    ev.x = x;
    ev.y = y;

    node_for_each(node, &gtbox.mouse_list) {
        wi = node->wi;

        if (wi->on_mouse == NULL) {
            // TODO: remove from mouse_list
            continue;
        }

        if (!wi->visible) continue;

        // TODO: new widget at head of mouse_list
        if (mouse_hit_widget(wi, x, y)) {
            if (new_pos && !new_key) {
                if (last_wi && last_wi != wi) {
                    ev.ev = TBOX_EVENT_MOUSE_MOVEOUT;
                    last_wi->on_mouse(last_wi, &ev);
                    tbox_log(TBOX_LOG_INFO, "mouse movout\n");
                }

                ev.ev = TBOX_EVENT_MOUSE_MOVEIN;
                wi->on_mouse(wi, &ev);
                tbox_log(TBOX_LOG_INFO, "mouse movin\n");
                last_wi = wi;
                return;
            }

            if (new_key) {
                if (key == TBOX_KEY_MOUSE_RELEASE) {
                    ev.ev = TBOX_EVENT_MOUSE_UP;
                    wi->on_mouse(wi, &ev);
                    tbox_log(TBOX_LOG_INFO, "mouse up\n");

                    if (last_wi == wi) {
                        ev.ev = TBOX_EVENT_MOUSE_CLICK;
                        wi->on_mouse(wi, &ev);
                        tbox_log(TBOX_LOG_INFO, "mouse click\n");
                    }

                    last_wi = NULL;
                } else if (key == TBOX_KEY_MOUSE_WHEEL_DOWN || key == TBOX_KEY_MOUSE_WHEEL_UP) {
                    ev.ev = TBOX_EVENT_MOUSE_WHEEL;
                    wi->on_mouse(wi, &ev);
                    tbox_log(TBOX_LOG_INFO, "wheel %s\n",
                        key == TBOX_KEY_MOUSE_WHEEL_DOWN ? "down" : "up");
                } else {
                    ev.ev = TBOX_EVENT_MOUSE_DOWN;
                    wi->on_mouse(wi, &ev);
                    tbox_log(TBOX_LOG_INFO, "mouse down\n");
                    last_wi = wi;
                }
                return;
            }
            // TODO: TBOX_EVENT_MOUSE_MOVEOUT
            return;
        }
    }

    if (last_wi) {
        ev.ev = TBOX_EVENT_MOUSE_MOVEOUT;
        last_wi->on_mouse(last_wi, &ev);
        tbox_flush();
        tbox_log(TBOX_LOG_INFO, "mouse movout\n");
        last_wi = NULL;
    }
}

inline int tbox_width(void) {
    return global.width;
}

inline int tbox_height(void) {
    return global.height;
}

int tbox_init(struct tbox_conf *conf) {
    memset(&gtbox, 0, sizeof(gtbox));
    log_init(conf);
    tbox_log(TBOX_LOG_INFO, "tbox_init\n");

    tb_init();
    tb_set_input_mode(TB_INPUT_ESC | TB_INPUT_MOUSE);
    tb_set_output_mode(TB_OUTPUT_TRUECOLOR);
    tb_clear();
    tb_present();

    node_init_head(&gtbox.widget_list);
    node_init_head(&gtbox.layout_list);
    node_init_head(&gtbox.mouse_list);
    gtbox.status = TBOX_STATUS_OK;

    return 0;
}

void tbox_exit(void) {
    tb_shutdown();
    gtbox.status = TBOX_STATUS_EXIT;
    log_deinit();
}

inline int tbox_clear(void) {
    return tb_clear();
}

inline int tbox_flush(void) {
    return tb_present();
}

inline int tbox_update(void) {
    return 0;
}

/* draw widget and inner widgets */
inline int draw_widget_group(tbox_widget_t *wi) {
    (void)wi;
    return 0;
}

int tbox_poll(void) {
    node_t *node;
    node_for_each(node, &gtbox.widget_list) {
        if (node->wi->on_mouse) {
            node_t *nd = (node_t *)tbox_malloc(sizeof(node_t));
            if (nd == NULL) break;
            memset(nd, 0, sizeof(node_t));
            nd->wi = node->wi;
            node_append(&gtbox.mouse_list, nd);
        }
    }

    draw();

    struct tb_event ev;

    int ret = 0;
    while (1) {
        ret = tb_poll_event(&ev);

        if (ret != TB_OK) {
            if (ret == TB_ERR_POLL && tb_last_errno() == EINTR) {
                /* poll was interrupted, maybe by a SIGWINCH; try again */
                continue;
            }
            /* some other error occurred; bail */
            break;
        }

        switch (ev.type) {
            case TB_EVENT_KEY:
                if (ev.key == TB_KEY_CTRL_C || ev.ch == 'q') {
                    tb_shutdown();
                }
                // printf("\r key=%x\n", ev.key); // XXX: everykey is 0
                break;
            case TB_EVENT_RESIZE:
                draw();
                break;
            case TB_EVENT_MOUSE:
                handle_mouse(ev.key, ev.x, ev.y);
                // draw();
                break;
            default:
                break;
        }
    }

    tbox_exit();

    return global.last_errno;
}
